/*
 * Copyright 2014 OGIS-RI All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jp.co.ogis_ri.optaplanner.vehiclerouting.domain.solver;

import jp.co.ogis_ri.optaplanner.vehiclerouting.domain.core.ChainedTimeWindowedStep;
import jp.co.ogis_ri.optaplanner.vehiclerouting.domain.core.TimeWindowedStep;

import org.apache.commons.lang3.ObjectUtils;
import org.optaplanner.core.impl.domain.variable.listener.VariableListener;
import org.optaplanner.core.impl.score.director.ScoreDirector;

public class ArrivalTimeUpdatingVariableListener implements
		VariableListener<ChainedTimeWindowedStep> {

	@Override
	public void beforeEntityAdded(final ScoreDirector scoreDirector,
			final ChainedTimeWindowedStep cargo) {
		// Do nothing
	}

	@Override
	public void afterEntityAdded(final ScoreDirector scoreDirector,
			final ChainedTimeWindowedStep cargo) {
		this.updateVehicle(scoreDirector, cargo);
	}

	@Override
	public void beforeVariableChanged(final ScoreDirector scoreDirector,
			final ChainedTimeWindowedStep cargo) {
		// Do nothing
	}

	@Override
	public void afterVariableChanged(final ScoreDirector scoreDirector,
			final ChainedTimeWindowedStep cargo) {
		this.updateVehicle(scoreDirector, cargo);
	}

	@Override
	public void beforeEntityRemoved(final ScoreDirector scoreDirector,
			final ChainedTimeWindowedStep cargo) {
		// Do nothing
	}

	@Override
	public void afterEntityRemoved(final ScoreDirector scoreDirector,
			final ChainedTimeWindowedStep cargo) {
		// Do nothing
	}

	protected void updateVehicle(final ScoreDirector scoreDirector,
			final ChainedTimeWindowedStep sourceCargo) {

		// ひとつ前の地点を出発する時間を取得
		TimeWindowedStep previousStep = sourceCargo.getPreviousStep();
		Integer departureTime = previousStep == null ? null : previousStep
				.getDepartureTime();

		ChainedTimeWindowedStep shadowCargo = sourceCargo;

		// この地点への到着時刻を計算
		Integer arrivalTime = this.calculateArrivalTime(shadowCargo,
				departureTime);

		while (shadowCargo != null
				&& ObjectUtils.notEqual(shadowCargo.getArrivalTime(),
						arrivalTime)) {

			// 計算した到着時間を設定する
			scoreDirector.beforeVariableChanged(shadowCargo, "arrivalTime");
			shadowCargo.setArrivalTime(arrivalTime);
			scoreDirector.afterVariableChanged(shadowCargo, "arrivalTime");

			// 次の地点の到着時間を計算するために
			departureTime = shadowCargo.getDepartureTime();
			shadowCargo = shadowCargo.getNextStep();
			arrivalTime = this.calculateArrivalTime(shadowCargo, departureTime);
		}

	}

	private Integer calculateArrivalTime(final ChainedTimeWindowedStep cargo,
			final Integer previousDepartureTime) {
		if (cargo == null) {
			return null;
		}
		if (previousDepartureTime == null) {
			// 前の地点の出発時間が無い場合
			// 出荷準備が可能な時間、または、移動に必要な時間の、の大きい方を選択する
			return Math.max(cargo.getReadyTime(),
					cargo.getTravelTimeToPreviousStep());
		}

		// 移動距離を移動に必要な時間とし、前の地点の出発時間に移動に必要な時間を足す
		return previousDepartureTime + cargo.getTravelTimeToPreviousStep();
	}

}
